Skip to content

9.27 事务和工作单元

📝 模块更新日志 新特性*

+ 新增 远程请求上传文件时可以配置是否对文件名进行转义参数 `escape` 4\.9\.4\.1 ⏱️2024\.06\.17 [60836ff](https://gitee.com/dotnetchina/Furion/commit/60836ff6020ba479a19d5d8f4910a4dfda7f6fe7)
+ 新增 `[UnitOfWork]` 工作单元特性输出详细的事务日志 4\.9\.4\.1 ⏱️2024\.06\.17 [ef4cb3a](https://gitee.com/dotnetchina/Furion/commit/ef4cb3a32ef8d6451f6866945e35eaf957933907) [\#IA457S](https://gitee.com/dotnetchina/Furion/issues/IA457S)
  • 问题修复

    • 修复 Scoped.CreateUowAsync 在一些特定情况下会出现空异常情况 4.9.5.5 ⏱️2024.08.24 212badc
    • 修复 Razor Pages 项目 [UnitOfWork] 工作单元日志打印不同步问题 4.9.4.8 ⏱️2024.07.28 ff42327
    • 修复 Scoped.CreateUowAsync 内部异常上层应用不能捕获问题 4.9.1.8 ⏱️2023.11.30 3c859e8
    • 修复 Scoped.CreateUowAsync 作用域工作单元异常无法回滚问题 4.8.8.44 ⏱️2023.09.23 #I833I9

9.27.1 事务

事务指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行

简单的说,事务就是并发控制的单位,是用户定义的一个操作序列。 而一个逻辑工作单元要成为事务,就必须满足 ACID 属性。

  • A:原子性(Atomicity):事务中的操作要么都不做,要么就全做
  • C:一致性(Consistency):事务执行的结果必须是从数据库从一个一致性状态转换到另一个一致性状态
  • I:隔离性(Isolation):一个事务的执行不能被其他事务干扰
  • D:持久性(Durability):一个事务一旦提交,它对数据库中数据的改变就应该是永久性的

9.27.2 工作单元

简单来说,就是为了保证一次完整的功能操作所产生的一系列提交数据的完整性,起着事务的作用。在计算机领域中,工作单元通常用 UnitOfWork 名称表示。

通常我们保证用户的每一次请求都是处于在一个功能单元中,也就是工作单元。

9.27.3 如何使用

9.27.3.1 [UnitOfWork] 自动管理

Furion 框架中,我们只需要在控制器 Action 中贴 [UnitOfWork] 特性即可开启工作单元模式,保证了每一次请求都是一个 工作单元,要么同时成功,要么同时失败。

  • 单库操作

下面方式支持所有关系型数据库类型

[UnitOfWork]    // 由于出现错误,所以所有数据库变更都会自动回滚  
public async Task 测试环境事务(int id)  
{  
    // 各种奇葩数据库操作  
    await _personRepository.DeleteNowAsync(id);  

    // 其他数据库操作。。  

    // 故意出错  
    var d = await _personRepository.SqlQueriesAsync("select * from persion2 d");  
}  

  • 多库操作

支持各种奇葩的 ORM,包括 ADO.NETEFCore 等第三方,支持所有关系型数据库类型但不支持 Sqlite

[UnitOfWork(UseAmbientTransaction = true)]    // 由于出现错误,所以所有数据库变更都会自动回滚  
public async Task 测试环境事务(int id)  
{  
    // 各种奇葩数据库操作  
    await _personRepository.DeleteNowAsync(id);  

    // 其他数据库操作。。  

    // 故意出错  
    var d = await _personRepository.SqlQueriesAsync("select * from persion2 d");  
}  

  • UnitOfWork 内置配置:
    • UseAmbientTransaction:是否开启分布式环境事务,bool 类型,默认 false不支持 Sqlite
    • TransactionScope:配置分布式环境事务范围,TransactionScopeOption 类型,当 UseAmbientTransactiontrue 有效
    • TransactionIsolationLevel:配置分布式环境事务隔离级别,IsolationLevel 类型,当 UseAmbientTransactiontrue 有效
    • TransactionTimeout:配置分布式环境事务执行超时时间,int 类型,当 UseAmbientTransactiontrue 有效
    • TransactionScopeAsyncFlow:配置分布式环境事务异步流支持,TransactionScopeAsyncFlowOption 类型,当 UseAmbientTransactiontrue 有效
    • EnsureTransaction:强制使字符串 sql 拓展事务有效,bool 类型,默认 false

版本说明以下内容仅限 Furion 3.7.3 + 版本使用。

如使用非 EFCore ORM 框架,可实现 IUnitOfWork 接口之后调用 services.AddUnitOfWork<TUnitOfWork>() 注册即可,如示例代码:

using Microsoft.AspNetCore.Mvc.Filters;  

namespace Furion.DatabaseAccessor;  

/// <summary>  
/// SqlSugar 工作单元实现  
/// </summary>  
public sealed class SqlSugarUnitOfWork : IUnitOfWork  
{  
    /// <summary>  
    /// SqlSugar 对象  
    /// </summary>  
    private readonly ISqlSugarClient _sqlSugarClient;  

    /// <summary>  
    /// 构造函数  
    /// </summary>  
    /// <param name="sqlSugarClient"></param>  
    public SqlSugarUnitOfWork(ISqlSugarClient sqlSugarClient)  
    {  
        _sqlSugarClient = sqlSugarClient;  
    }  

    /// <summary>  
    /// 开启工作单元处理  
    /// </summary>  
    /// <param name="context"></param>  
    /// <param name="unitOfWork"></param>  
    /// <exception cref="NotImplementedException"></exception>  
    public void BeginTransaction(FilterContext context, UnitOfWorkAttribute unitOfWork)  
    {  
        _sqlSugarClient.AsTenant().BeginTran();  
    }  

    /// <summary>  
    /// 提交工作单元处理  
    /// </summary>  
    /// <param name="resultContext"></param>  
    /// <param name="unitOfWork"></param>  
    /// <exception cref="NotImplementedException"></exception>  
    public void CommitTransaction(FilterContext resultContext, UnitOfWorkAttribute unitOfWork)  
    {  
        _sqlSugarClient.AsTenant().CommitTran();  
    }  

    /// <summary>  
    /// 回滚工作单元处理  
    /// </summary>  
    /// <param name="resultContext"></param>  
    /// <param name="unitOfWork"></param>  
    /// <exception cref="NotImplementedException"></exception>  
    public void RollbackTransaction(FilterContext resultContext, UnitOfWorkAttribute unitOfWork)  
    {  
        _sqlSugarClient.AsTenant().RollbackTran();  
    }  

    /// <summary>  
    /// 执行完毕(无论成功失败)  
    /// </summary>  
    /// <param name="context"></param>  
    /// <param name="resultContext"></param>  
    /// <exception cref="NotImplementedException"></exception>  
    public void OnCompleted(FilterContext context, FilterContext resultContext)  
    {  
        _sqlSugarClient.Dispose();  
    }  
}  

之后注册即可:

services.AddUnitOfWork<SqlSugarUnitOfWork>();  

小知识-如何判断是否开启了分布式环境事务有时候我们自定义了工作单元之后,个别 ORM 不支持分布式环境事务,那么就会出现执行错误,我们可以通过 System.Transactions.Transaction.Current != null 来判断是否启用了分布式环境事务,不等于 null 则为启用,否则未启用。

9.27.3.2 EnsureTransaction() 方法 ✨

有些时候我们通过静态类或者其他方式不小心创建了新的 DbContext 实例,这时候贴了 [UnitOfWork] 也不见起效,这时候可以通过以下方法来确认事务是否有效:

repository.EnsureTransaction();  

如果不喜欢手动方式也可以通过 [UnitOfWork(true)] 开启此功能。

该方法会将当前仓储添加到数据库上下文池中,并确保事务可用。

9.27.3.2 手动管理

  • 示例一
  • 示例二
  • 示例三(分布式)
// 开启事务  
using (var transaction = _testRepository.Database.BeginTransaction())  
{  
    try  
    {  
        _testRepository.Insert(new Blog { Url = "http://blogs.msdn.com/dotnet" });  
        _testRepository.SaveNow();  

        _testRepository.Insert(new Blog { Url = "http://blogs.msdn.com/visualstudio" });  
        _testRepository.SaveNow();  

        var blogs = _testRepository.Entity  
                .OrderBy(b => b.Url)  
                .ToList();  

        // 提交事务  
        transaction.Commit();  
     }  
     catch (Exception)  
     {  
        // 回滚事务  
        // transaction.RollBack(); // 新版本自动回滚了  
     }  
}  

var options = new DbContextOptionsBuilder<DefaultDbContext>()  
    .UseSqlServer(new SqlConnection(connectionString))  
    .Options;  

// 创建连接字符串  
using (var context1 = new DefaultDbContext(options))  
{  
    // 开启事务  
    using (var transaction = context1.Database.BeginTransaction())  
    {  
        try  
        {  
            _testRepository.Insert(new Blog { Url = "http://blogs.msdn.com/dotnet" });  
            _testRepository.SaveNow();  

            context1.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });  
            context1.SaveChanges();  

            // 创建新的连接对象  
            using (var context2 = new DefaultDbContext(options))  
            {  
                // 共享连接事务  
                context2.Database.UseTransaction(transaction.GetDbTransaction());  

                var blogs = context2.Blogs  
                    .OrderBy(b => b.Url)  
                    .ToList();  
            }  

            // 提交事务  
            transaction.Commit();  
        }  
        catch (Exception)  
        {  
            // 回滚事务  
            // transaction.RollBack(); // 新版本自动回滚了  
        }  
    }  
}  

// 开启分布式事务  
// 如果事务包裹的代码中包含异步 async/await,那么需要设置 TransactionScopeAsyncFlowOption.Enabled = true  
using (var scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))  
{  
    using (var connection = new SqlConnection(connectionString))  
    {  
        connection.Open();  

        try  
        {  
            // 这里是 Ado.NET 操作  
            var command = connection.CreateCommand();  
            command.CommandText = "DELETE FROM dbo.Blogs";  
            command.ExecuteNonQuery();  

            // 创建EF Core 数据库上下文  
            var options = new DbContextOptionsBuilder<BloggingContext>()  
                .UseSqlServer(connection)  
                .Options;  
            using (var context = new BloggingContext(options))  
            {  
                context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });  
                context.SaveChanges();  
            }  

            // 框架封装的仓储  
            _testRepository.Insert(new Blog { Url = "http://blogs.msdn.com/dotnet" });  
            _testRepository.SaveChanges();  

           // 提交事务  
            scope.Complete();  
        }  
        catch (System.Exception)  
        {  
            // 自动回滚  
        }  
    }  
}  

9.27.3.3 EnableRetryOnFailure 错误处理

特别注意如果使用的是 EFCore 数据库且启用了 EnableRetryOnFailure() 功能,那么 [UnitOfWork] 将抛出以下错误:

InvalidOperationException: The configured execution strategy 'SqlServerRetryingExecutionStrategy' does not support user-initiated transactions.  
Use the execution strategy returned by 'DbContext.Database.CreateExecutionStrategy()' to execute all the operations in the transaction as a retriable unit.  

这时候需手动事务,并创建执行策略:CreateExecutionStrategy,详情请参考官方说明:https://learn.microsoft.com/zh-cn/ef/core/miscellaneous/connection-resiliency

using var db = new BloggingContext();  
var strategy = db.Database.CreateExecutionStrategy();  

strategy.Execute(  
    () =>  
    {  
        using var context = new BloggingContext();  
        using var transaction = context.Database.BeginTransaction();  

        context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });  
        context.SaveChanges();  

        context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });  
        context.SaveChanges();  

        transaction.Commit();  
    });  

9.27.4 工作单元特性说明

9.27.4.1 [UnitOfWork] 特性

[UnitOfWork] 特性只能用于控制器的 Action 中,一旦贴了 [UnitOfWork] 特性后,那么该请求自动启用工作单元模式,要么成功,要么失败。

9.27.4.2 [ManualCommit] 特性

默认情况下,Furion 框架会在一次成功请求之后自动调用 SaveChanges() 方法,如果选择手动调用 SaveChanges() 方法,可以在控制器 Action 中贴 [ManualCommit] 特性即可。

9.27.5 反馈与建议

与我们交流给 Furion 提 Issue


了解更多想了解更多 事务 知识可查阅 EF Core - 使用事务 章节。